#include "Task.hpp"
#include <string>
#include <vector>
#include <cassert>
#include <unistd.h>
#include <stdlib.h>
#include <ctime>
#include <sys/types.h>
#include <sys/wait.h>

//从头开始写这个代码，也能更好的理解先描述再组织的强大
//最终做到了父进程只负责往管道写任务码，只要对应管道的子进程读到任务码就会去执行，这样就做到了多进程的并发执行

const int processnum=5;
std::vector<task_t> tasks;//tasks存放的时候指向每个task函数的函数指针

//将管道描述起来
class channel
{
public:
    channel(int cmdfd,int slaverid,const std::string& processname)
    :_cmdfd(cmdfd),_slaverid(slaverid),_processname(processname)
    {}
public:
    int _cmdfd;               //发送任务的文件描述符，管道的写端
    pid_t _slaverid;          //子进程的PID
    std::string _processname; //子进程的名字---方便打印日志
};


void slaver()
{
    //read(0); 
    while(true)
    {
        int cmdcode=0;
        int n=read(0,&cmdcode,sizeof(int));//如果父进程不给子进程发送数据呢？子进程阻塞等待！
        if(n==sizeof(int))
        {
            //执行cmdcode对应的任务列表
            std::cout<<"slaver say@ get a command: "<<getpid()<<" : cmdcode: "<<cmdcode<<std::endl;
            if(cmdcode>=0&&cmdcode<tasks.size()) tasks[cmdcode]();//函数指针调用和函数的语法一样，只是多了一层在vector中[]找到具体哪一个函数指针
        }
        if(n==0) break;//说明read读到了文件结尾，也就是写端关闭了
    }
    
}


//输入：const &
//输出：*
//输入输出：&
void InitProcessPool(std::vector<channel>* channels)
{
    //version 2:确保每个管道都只有一个写端
    std::vector<int> oldfds;
    for(int i=0;i<processnum;i++)//创建多个子进程，每个子进程都与父进程有一个管道
    {
        int pipefd[2];//临时空间
        int n=pipe(pipefd);
        assert(!n);//管道创建成功返回0，这里断言n一定为0；assert只在Debug下有效,所以gcc编译时可以加上-g选项，保留调试信息
        (void)n;

        pid_t id=fork();
        if(id==0)//child
        {
            //在创建子进程后，将继承自父进程的上一个管道的写端给关掉；第一次vector为空，不会执行for循环；vector是父进程创建的，子进程能看到
            std::cout<<"child: "<<getpid()<<" close history fd: ";
            for(auto fd : oldfds) 
            {
                std::cout<<fd<<" ";
                close(fd); 
            }
            std::cout<<"\n";

            close(pipefd[1]);//子进程为读端

            //slaver(pipefd[0]);//传管道的读端，让子进程拿到管道的内容去执行工作，也可以使用dup2
            dup2(pipefd[0],0);//将当前子进程的标准输入替换成管道的读端,此时子进程想从管道里面拿任务就从0号文件里面拿就可以
            close(pipefd[0]);
            slaver();
            std::cout<<"process : "<<getpid()<<" quit"<<std::endl;
            exit(0);
        }

        //father
        close(pipefd[0]);

        //添加channel字段
        std::string name="process-"+std::to_string(i);
        channels->push_back(channel(pipefd[1],id,name));//父进程往每个管道的写端写，父进程就可以根据不同的管道访问不同的子进程

        oldfds.push_back(pipefd[1]);//保存父进程打开的当前管道的写端，方便下一次创建子进程的时候关闭，防止被继承

        sleep(1);
    }
}

void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}


//随机数选择进程
// void CtrlSlaver(const std::vector<channel>& channels)
// {
//     while(true)
//     {
//         //1.选择任务
//         int cmdcode=rand()%tasks.size();

//         //2.选择进程,转换成管道的vector里面选一个元素，也就是找一个下标
//         //为了避免选择进程时造成有的进程一直闲着，有的进程一直被选中，所以要考虑负载均衡的问题，解决方法：
//         //a.随机数  b.轮转（轮询）
//         int processpos=rand()%channels.size();//采用随机数

//         std::cout<<"father say: "<<"cmdcode: "<<cmdcode<<" already sendto "<<channels[processpos]._slaverid<<" process name: "
//                  <<channels[processpos]._processname<<std::endl;

//         //3.发送任务,write第一个参数就是确定父进程往哪一个管道的写端写
//         write(channels[processpos]._cmdfd,&cmdcode,sizeof(cmdcode));//当前写也是固定大小，4字节即一个任务码

//         sleep(1);
//     }
// }

//轮询选择进程
void CtrlSlaver(const std::vector<channel>& channels)
{
    int which=0;
    while(true)
    {
        //1.选择任务
        //int cmdcode=rand()%tasks.size();//随机选择任务

        //手动选择任务
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;
        // select > 0&& select < 5
        int cmdcode = select - 1;//注意tasks[cmdcode]()，cmdcode是下标，所以需要-1
        

        //2.选择进程,转换成管道的vector里面选一个元素，也就是找一个下标
        //为了避免选择进程时造成有的进程一直闲着，有的进程一直被选中，所以要考虑负载均衡的问题，解决方法：
        //a.随机数  b.轮转（轮询）
        //int processpos=rand()%channels.size();//采用随机数

        std::cout<<"father say: "<<"cmdcode: "<<cmdcode<<" already sendto "<<channels[which]._slaverid<<" process name: "
                 <<channels[which]._processname<<std::endl;

        //3.发送任务,write第一个参数就是确定父进程往哪一个管道的写端写
        write(channels[which]._cmdfd,&cmdcode,sizeof(cmdcode));//当前写也是固定大小，4字节即一个任务码

        which++;
        which%=channels.size();

        //sleep(1);
    }
}

void QuitProcess(const std::vector<channel>& channels)
{
    for(const auto& e:channels)
    {
        close(e._cmdfd);
        waitpid(e._slaverid,nullptr,0);
    }


    // for(const auto& e:channels) close(e._cmdfd);//遍历放管道的容器，范围for的循环变量加上&就是说明变量就是channels，所以改变e就是改变channels
    // //sleep(10);//所有管道的写端都关了，read返回0，子进程走到exit(0)，子进程退出，所有子进程在这10s内都是僵尸，想验证这个现象需要将ctrlSlaver不能写成死循环
    // for(const auto& e:channels) waitpid(e._slaverid,nullptr,0);
    // //sleep(10);//此时只剩父进程了
    
    //version1-倒着回收,针对一个管道有多个写端
    // int last=channels.size()-1;
    // for(int i=last;i>=0;i--)
    // {
    //     close(channels[i]._cmdfd);
    //     waitpid(channels[i]._slaverid,nullptr,0);
    // }
}

void Debug(const std::vector<channel>& channels)
{
    //test
    for(const auto& e:channels)
    {
        std::cout<<e._cmdfd<<" "<<e._slaverid<<" "<<e._processname<<std::endl;
    }
}


int main()
{
    LoadTask(&tasks);//假设是加载任务
    srand((unsigned int)time(nullptr)^getpid()^1023);//种随机数的种子
    //将管道组织起来
    std::vector<channel> channels;

    //1.初始化
    InitProcessPool(&channels);
    //Debug(channels);

    //2.开始控制子进程,父进程往管道里写任务码
    CtrlSlaver(channels);
   
    //3.清理收尾
    QuitProcess(channels);

    //sleep(1000);
    return 0;

}